/*
 * Scala.js (https://www.scala-js.org/)
 *
 * Copyright EPFL.
 *
 * Licensed under Apache License 2.0
 * (https://www.apache.org/licenses/LICENSE-2.0).
 *
 * See the NOTICE file distributed with this work for
 * additional information regarding copyright ownership.
 */

package org.scalajs.linker.standard

import org.scalajs.ir.Names.ClassName
import org.scalajs.ir.Trees.TopLevelExportDef

import org.scalajs.linker.interface.ModuleInitializer

/** A set of linked Scala.js modules.
 *
 *  The Scala.js linker distinguishes three types of modules:
 *
 *  - External Modules: Modules that are imported through native JS
 *    types/members. These are not generated by Scala.js but need to be provided
 *    by the user. For example, the Node.js `fs` module.
 *  - Public Modules: Scala.js-produced modules intended for import by user
 *    code. Only these modules may hold top level exports (but may contain more).
 *    Public modules may not be imported by other Scala.js produced modules.
 *  - Internal Modules: Scala.js-produced modules only relevant for splitting.
 *    These are only intended to be imported by other Scala.js-produced modules.
 *
 *  Note that a ModuleSet may contain no modules at all. This happens if there
 *  are no public modules.
 */
final class ModuleSet private[linker] (
    val coreSpec: CoreSpec,
    val modules: List[ModuleSet.Module],

    /** Abstract classes may not have any definitions, but are still required
     *  for proper code generation.
     *
     *  For example, a native JS class that is needed for its load spec.
     */
    val abstractClasses: List[LinkedClass]
) {
  require(modules.isEmpty || modules.count(_.isRoot) == 1,
      "Must have exactly one root module")
}

object ModuleSet {
  /* ModuleID is not an AnyVal because it is used a lot in Maps as key.
   * An AnyVal would cause a lot of boxing/unboxing.
   */
  final class ModuleID(val id: String) {
    override def toString(): String = s"ModuleID($id)"

    override def equals(that: Any): Boolean = that match {
      case that: ModuleID => this.id == that.id
      case _              => false
    }

    override def hashCode(): Int = id.##
  }

  object ModuleID {
    def apply(id: String): ModuleID = new ModuleID(id)
  }

  final class Module(
      val id: ModuleID,
      val internalDependencies: Set[ModuleID],
      val externalDependencies: Set[String],
      val public: Boolean,
      val classDefs: List[LinkedClass],
      val topLevelExports: List[LinkedTopLevelExport],
      val initializers: Seq[ModuleInitializer.Initializer]
  ) {
    require(public || topLevelExports.isEmpty,
        "Only public modules may have top-level exports")

    require(!internalDependencies.contains(id),
        "A module may not depend on itself")

    def isRoot: Boolean = internalDependencies.isEmpty
  }
}
